package model

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/iancoleman/orderedmap"

	"github.com/invopop/jsonschema"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/bsontype"
)

type (
	VolumeMounts []VolumeWrapper

	VolumeWrapper struct {
		StrValue    *string
		VolumeValue *Volume
	}

	Volume struct {
		TypeVolume string            `json:"type,omitempty" bson:"type,omitempty"`
		Source     VolumeName        `json:"source,omitempty" bson:"source,omitempty"`
		Target     string            `json:"target,omitempty" bson:"target,omitempty"`
		ReadOnly   bool              `json:"read_only,omitempty" bson:"read_only"`
		Tmpfs      map[string]string `json:"tmpfs,omitempty" bson:"tmpfs"`
	}

	VolumeConfiguration struct {
		VolumeSource  VolumeName
		ContainerPath string
		ReadOnly      bool
		IsTemp        bool
		InMemory      bool
	}
)

const (
	defaultSource = "volume-%d"
)

func (VolumeMounts) JSONSchema() *jsonschema.Schema {
	properties := orderedmap.New()
	tmpfsProperties := orderedmap.New()

	properties.Set("type", jsonschema.Schema{Type: "string"})
	properties.Set("source", jsonschema.Schema{Type: "string"})
	properties.Set("target", jsonschema.Schema{Type: "string"})
	properties.Set("read_only", jsonschema.Schema{Type: "boolean"})
	tmpfsProperties.Set("size", jsonschema.Schema{Type: "integer"})
	properties.Set("tmpfs", jsonschema.Schema{Type: "object", Properties: tmpfsProperties})

	return &jsonschema.Schema{
		Type: "array",
		Items: &jsonschema.Schema{
			OneOf: []*jsonschema.Schema{
				{Type: "string", Format: "volumes"},
				{
					Type:       "object",
					Properties: properties,
				},
			},
		},
	}
}

func (v *VolumeWrapper) MarshalJSON() ([]byte, error) {
	if v == nil {
		return []byte("null"), nil
	}

	if v.VolumeValue != nil {
		return json.Marshal(v.VolumeValue)
	}

	if v.StrValue != nil {
		return json.Marshal(v.StrValue)
	}

	return []byte("null"), nil
}

func (v *VolumeWrapper) UnmarshalJSON(bytes []byte) error {
	if string(bytes) == "null" {
		return nil
	}

	for _, char := range string(bytes) {
		switch char {
		case '{':
			result := &Volume{}
			err := json.Unmarshal(bytes, result)
			if err != nil {
				return fmt.Errorf("unmarshall to struct volumes failed: %w", err)
			}

			*v = VolumeWrapper{
				VolumeValue: result,
			}
			return nil
		case '"':
			var result string
			err := json.Unmarshal(bytes, &result)
			if err != nil {
				return fmt.Errorf("unmarshall to string volumes failed: %w", err)
			}

			*v = VolumeWrapper{
				StrValue: &result,
			}
			return nil
		default:
			return fmt.Errorf("unmarshall to struct volumes failed. it should be string or object: %v", char)

		}
	}

	return nil
}

func (v *VolumeWrapper) MarshalBSONValue() (bsontype.Type, []byte, error) {
	if v == nil {
		return bsontype.Null, []byte{}, nil
	}

	if v.VolumeValue != nil {
		return bson.MarshalValue(v.VolumeValue)
	}

	if v.StrValue != nil {
		return bson.MarshalValue(v.StrValue)
	}

	return bsontype.Null, []byte{}, nil
}

func (v *VolumeWrapper) UnmarshalBSONValue(t bsontype.Type, data []byte) error {
	raw := bson.RawValue{Type: t, Value: data}

	switch t {
	case bsontype.Null:
		*v = VolumeWrapper{StrValue: nil, VolumeValue: nil}
		return nil
	case bsontype.String:
		result := raw.StringValue()
		*v = VolumeWrapper{StrValue: &result}
		return nil
	case bsontype.EmbeddedDocument:
		result := Volume{}
		if err := raw.Unmarshal(&result); err != nil {
			return fmt.Errorf("cannot unmarshall volumes: %w", err)
		}
		*v = VolumeWrapper{VolumeValue: &result}
		return nil
	default:
		return fmt.Errorf("cannot unmarshall volumes with type '%s'", t)
	}
}

func (v *VolumeWrapper) ToVolumeConfiguration() (VolumeConfiguration, error) {
	result := VolumeConfiguration{}
	result.ReadOnly = false

	if v.StrValue != nil {
		value := *v.StrValue

		result.InMemory = false
		parts := strings.Split(value, ":")

		switch len(parts) {
		case 0:
			return VolumeConfiguration{}, fmt.Errorf("failed to parse volume configuration '%s': expected at list 1 part with delimiter ':'", value)
		case 1:
			result.VolumeSource = VolumeName(fmt.Sprintf(strings.Trim(defaultSource, " "), 0))
			result.ContainerPath = value
			result.IsTemp = true
			break
		default:
			result.VolumeSource = VolumeName(strings.Trim(parts[0], " "))
			result.ContainerPath = strings.Trim(parts[1], " ")
			result.IsTemp = false
			if len(parts) > 2 {
				if strings.Trim(parts[2], " ") == "ro" {
					result.ReadOnly = true
				}
			}
			break
		}

		return result, nil
	}

	if v.VolumeValue != nil {
		value := *v.VolumeValue

		switch value.TypeVolume {
		case "volume":
			result.VolumeSource = value.Source
			result.ContainerPath = value.Target
			result.ReadOnly = value.ReadOnly
			break
		case "tmpfs":
			result = VolumeConfiguration{
				VolumeSource:  value.Source,
				ContainerPath: value.Target,
				ReadOnly:      value.ReadOnly,
				IsTemp:        true,
				InMemory:      true,
			}
			break
		default:
			return VolumeConfiguration{}, fmt.Errorf("type '%s' is not supported type for volume", value.TypeVolume)
		}

		return result, nil
	}

	return VolumeConfiguration{}, fmt.Errorf("volume wrapper is empty")
}
